- Published on
lagacy code
除非你参与过遗留项目,否则你不能称自己为资深工程师
一张老旧压力阀的深褐色照片
每个人都讨厌参与遗留项目,我也不例外。命运捉弄人,最近我手上就摊上了一个。虽然参与这个项目并没有减少我对遗留项目的厌恶,但它确实帮助我更深入地理解了我们今天使用的流程和实践。
我很自豪能成为一个采用大多数最佳实践的团队的一员:
- 编写简洁的代码和自动化测试
- 参与代码合并请求(Pull Request)和任务评审
- 应用程序在合并到主分支的当天就能部署到生产环境
- 敏捷(Agile)方法得到了很好的实践
当然,也并非十全十美。代码合并请求有时会包含一些无关紧要的建议和讨论。运维团队通常会搞砸一些事情(或者至少我们开发人员喜欢这么想)。我们的产品负责人偶尔又想让我们快速推进一些“简单”的功能……。但总的来说,情况还是相当不错的。
一次 Ant “博物馆”之旅
由于我们团队表现出色,我们的开发速度被“借调”到公司另一部门管理的另一个产品上。我们对此并不太兴奋,因为这个项目使用的是旧版本的 Java,而且代码的编写方式也与我们习惯的不同。
任务是添加一些指标——一些简单的指标,比如应用程序是否正在运行、运行了多长时间、是否正在处理数据(速度是否够快)。项目本身处于维护模式,已经有一段时间没有添加新功能了。由于我们习惯于添加指标,这本应是小菜一碟。
我们开始动手后,首先注意到这个项目是用一种非常古老的技术构建的——Ant 构建文件。它们是庞大的 XML 文件,包含了如何构建相关项目的所有信息。例如,你需要指定项目需要被编译、测试和打包。所有细节,如源文件位置、目标文件位置和资源位置,都必须在这些文件中明确配置。这在许多编程语言中曾经非常普遍。你只需编写一次构建文件,然后在每个新项目中复制它,再修改直到它适用于新项目。
这是一个 Hello World 项目的 Ant 构建文件示例:
<project>
<target name="clean">
<delete dir="build"/>
</target>
<target name="compile">
<mkdir dir="build/classes"/>
<javac srcdir="src" destdir="build/classes"/>
</target>
<target name="jar">
<mkdir dir="build/jar"/>
<jar destfile="build/jar/HelloWorld.jar" basedir="build/classes">
<manifest>
<attribute name="Main-Class" value="oata.HelloWorld"/>
</manifest>
</jar>
</target>
<target name="run">
<java jar="build/jar/HelloWorld.jar" fork="true"/>
</target>
</project>
一定有更方便的管理方式,对吧?嗯,这就是“约定优于配置”理念诞生的原因,它建议开发者只需要指定应用程序中非传统的部分。现代构建工具已经采纳了这种模式,为开发者提供了可在需要时覆盖的默认设置。这就是为什么大多数 Java 源文件位于 src/main/java
目录下,而编译后的类文件位于 target
文件夹中——无需一遍又一遍地指定这些。
这让我们思考这种方法是否对我们当前的项目有帮助。我们有一个庞大的应用程序属性文件,其中大部分值都是相同的(例如,应用程序端口)。我们能否将同样的原则应用于我们的应用程序属性呢?大多数属性可以有默认值,这样我们的属性文件就不会那么庞大了。
我们习以为常的事情
回到我们的遗留项目——我们成功地构建和打包了我们的应用程序!繁琐的部分已经过去了,我们可以开始编码了。但是,我们如何将指标组件嵌入到遗留代码库中呢?我们以前认为这是理所当然的,因为我们的应用程序框架通常会处理它。
那么,将我们的指标组件注入到遗留代码的各个部分的最佳方法是什么呢?单例模式(Singletons)!嗯,这至少看起来是最简单的方法,但社区认为它们是反模式。为什么呢?我们钟爱的 XY 框架不也依赖单例模式吗?如果不是,那它用的是什么?什么是依赖注入(Dependency Injection)?它底层是如何运作的?
这些问题让我们开始思考那些我们习以为常的基本概念。在这种情况下,使用单例模式并非完全是个坏主意,因为大部分代码没有单元测试覆盖。尽管如此,除非代码完美无瑕,否则我们无法安心睡觉。我们尝试了另一种方法,最终得到了合理且简洁的代码——没有单例,也没有新的抽象。
开发人员的有限角色
剩下唯一的事情就是部署它,以便我们进行测试。当然,这又是一个问题——在这种情况下,我们不是部署或测试的人。部署必须由运维团队完成,测试必须由测试团队完成。为什么我们作为开发人员,不能管理从头到尾整个功能的交付,直到生产上线,而非要提交工单并等待其他团队处理后才能关闭我们的任务呢?
首先,我们无法避免手动测试,因为大部分代码没有测试覆盖。我们也无法自己部署应用程序,因为基础设施不允许我们这样做。
这让我们思考职责分离背后的原因,以及我们当前的方法为何更好。看到我们在这个项目上的交付前置时间(lead time)和周期时间(cycle time)远高于平时(我们花了几周时间才交付了通常几天就能完成的东西),证据有力地证明了这一点。
学习过时的实践以理解当前的实践
最终,在那个月底,我们的指标在生产环境中成功运行了。我对遗留项目的看法没有改变——我仍然讨厌它们,我也不指望你会比我更喜欢它们。
我们无法改变被分配到的项目,这就是我们不得不面对的现实。但我们可以改变的是我们对遗留项目的态度。我们没有自暴自弃,而是将其视为一个提出问题和学习的地方。
它教会了我们过去事情是如何完成的,以及为什么现在的方式有所不同。我们不仅仅是了解了最佳实践,还亲身体验了它们背后的发展历程。
一旦你拥有了这种根深蒂固的知识,其他开发者就会认识到这一点,并信任你的知识和专业技能。如果你想成为那样的人,最好准备好去钻研一些遗留项目。